import Vue from 'vue'
import { isString, isObject } from 'zui/src/utils/types'

// hasOwnProperty() 方法返回一个布尔值，表示对象自有属性（而不是继承来的属性）中是否具有指定的属性。
// https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/Reference/Global_Objects/Object/hasOwnProperty
// 使用 Object.create(null) 创建的对象不从 Object.prototype 继承，使得 hasOwnProperty() 不可访问
// Object.create(null) 创建的对象不继承任何属性或方法，包括 hasOwnProperty 方法。
const hasOwnProperty = Object.prototype.hasOwnProperty

/**
 * 一个空操作函数，不执行任何操作。
 *
 * 这个函数在执行时不会执行任何逻辑，通常用作回调函数的占位符，
 * 或者在默认情况下不需要执行任何操作时使用。
 *
 * @returns {void} 无返回值。
 */
export function noop() {}

// 判断对象是否拥有某个属性
/**
 * 判断对象是否拥有某个自身属性（非继承属性）。
 *
 * 该函数使用`hasOwnProperty`方法来判断对象`obj`是否拥有名为`key`的自身属性。
 * 这种方法比直接使用`obj.hasOwnProperty(key)`更为安全，
 * 因为它避免了由于`obj`自身可能被重写或遮蔽的`hasOwnProperty`方法导致的潜在问题。
 *
 * @param {Object} obj - 要检查的对象。
 * @param {string} key - 要检查的属性名。
 * @returns {boolean} 如果对象`obj`拥有名为`key`的自身属性，则返回`true`；否则返回`false`。
 */
export function hasOwn(obj, key) {
  return hasOwnProperty.call(obj, key)
}

/**
 * 将一个对象（_from）的属性扩展到另一个对象（to）上。
 *
 * @param {Object} to - 目标对象，用于接收扩展的属性。
 * @param {Object} _from - 源对象，其属性将被复制到目标对象上。
 * @returns {Object} - 返回扩展后的目标对象。
 *
 * 此函数会遍历源对象的所有可枚举属性，并将它们复制到目标对象上。
 * 如果目标对象上已经存在同名属性，则会被源对象的属性值覆盖。
 */
function extend(to, _from) {
  // 遍历源对象的所有可枚举属性（包括原型继承的属性）
  for (let key in _from) {
    to[key] = _from[key]
  }
  return to
}

/**
 * 将一个包含对象的数组转换为一个单一的对象。
 *
 * 该函数遍历输入数组中的每个元素，如果元素为对象（即真值），则将其属性扩展到结果对象中。
 *
 * @param {Array} arr - 包含对象的数组。数组中的每个元素都应该是一个对象，或者是一个在逻辑上被视为真的值（以避免将非对象元素添加到结果对象中）。
 * @returns {Object} - 包含输入数组中所有对象属性的单一对象。如果输入数组为空或只包含非对象元素，则返回一个空对象。
 */
export function toObject(arr) {
  var res = {}
  for (let i = 0; i < arr.length; i++) {
    if (arr[i]) {
      extend(res, arr[i])
    }
  }
  return res
}

// 通过 path 获取嵌套对象中的值
export const getValueByPath = function (object, prop) {
  // 如果没传入 prop 按照空串处理兜底
  prop = prop || ''

  // 把 prop 按照 . 分割成数组，这样就可以通过循环遍历获取到嵌套对象的值
  const paths = prop.split('.')

  // current 就类似遍历链表的指针 p
  let current = object
  // 单独定义 result 变量，防止没进去循环
  let result = null

  // 这里用 j 缓存 paths 的长度，这样可以减少每次循环都去计算 paths 长度的开销
  for (let i = 0, j = paths.length; i < j; i++) {
    // path 就相当于链表中的 next 指针
    const path = paths[i]
    if (!current) break

    // 如果是最后一个属性，就直接返回当前的值
    // 这里为什么要用 i === j - 1 来判断呢？因为 paths.length 是从 0 开始计数的
    if (i === j - 1) {
      result = current[path]
      break
    }
    current = current[path]
  }

  return result
}

// 上面函数的严格版（支持了索引器和成员操作符），
// formItem 中对 prop 的取值就是复用的下面这段逻辑
export function getPropByPath(obj, path, strict) {
  let tempObj = obj
  // 把 [index] 替换为 .index
  // 比如 [0] 替换为 .0
  path = path.replace(/\[(\w+)\]/g, '.$1')
  // 移除路径开头的 .点符号
  // 比如 .0.name 替换为 0.name
  path = path.replace(/^\./, '')

  // 按照 . 分割路径
  let keyArr = path.split('.')
  let i = 0
  for (let len = keyArr.length; i < len - 1; ++i) {
    // 如果 tempObj 不能存在说明路径是不合法的，应该跳出循环
    if (!tempObj && !strict) break
    // 取出路径数组中的 key
    let key = keyArr[i]
    // 校验 key 是否在该对象上
    if (key in tempObj) {
      tempObj = tempObj[key]
    } else {
      // 如果 strict 为 true，还要抛异常
      // 走到这里有两种情况，一种是 key 不存在 tempObj 上
      // 另一种是 key 的长度超过了 tempObj 的深度，即 tempObj 已经为 undefined
      if (strict) {
        throw new Error('please transfer a valid prop path to form item!')
      }
      break
    }
  }

  return {
    // 最后找到的对象
    o: tempObj,
    // 对象对应的键
    k: keyArr[i],
    // 对象对应的值
    // 非严格模式下不会抛出异常，如果 tempObj 为 undefined 还要单独处理下
    v: tempObj ? tempObj[keyArr[i]] : null
  }
}

// 通过 random 实现唯一 id
export const generateId = function () {
  // 生成 4 位 ID
  return Math.floor(Math.random() * 10000)
}

// 安全的判断两个数组中的值是否相等
export const valueEquals = (a, b) => {
  // see: https://stackoverflow.com/questions/3115982/how-to-check-if-two-arrays-are-equal-with-javascript
  if (a === b) return true
  if (!(a instanceof Array)) return false
  if (!(b instanceof Array)) return false
  if (a.length !== b.length) return false
  for (let i = 0; i !== a.length; ++i) {
    if (a[i] !== b[i]) return false
  }
  return true
}

// 转移正则表达式中的特殊字符
// | \ {} () [] ^ $ + * ? . 替换为转义形式，将他们前面加上 \
// \$& 表示最近一次匹配到的整个子串的引用
// https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/Reference/Global_Objects/String/replace#%E6%8C%87%E5%AE%9A%E5%AD%97%E7%AC%A6%E4%B8%B2%E4%BD%9C%E4%B8%BA%E6%9B%BF%E6%8D%A2%E9%A1%B9
export const escapeRegexpString = (value = '') =>
  String(value).replace(/[|\\{}()[\]^$+*?.]/g, '\\$&')

// TODO: use native Array.find, Array.findIndex when IE support is dropped
export const arrayFindIndex = function (arr, pred) {
  for (let i = 0; i !== arr.length; ++i) {
    if (pred(arr[i])) {
      return i
    }
  }
  return -1
}

export const arrayFind = function (arr, pred) {
  const idx = arrayFindIndex(arr, pred)
  return idx !== -1 ? arr[idx] : undefined
}

// coerce truthy value to array
export const coerceTruthyValueToArray = function (val) {
  if (Array.isArray(val)) {
    return val
  } else if (val) {
    return [val]
  } else {
    return []
  }
}

// 判断浏览器是不是 IE
export const isIE = function () {
  return !Vue.prototype.$isServer && !isNaN(Number(document.documentMode))
}

// 判断浏览器是不是 Edge
export const isEdge = function () {
  return !Vue.prototype.$isServer && navigator.userAgent.indexOf('Edge') > -1
}

// 判断浏览器是不是 Firefox
export const isFirefox = function () {
  return !Vue.prototype.$isServer && !!window.navigator.userAgent.match(/firefox/i)
}

// 检查 style 中是否有 transform、transition、animation 的属性
// 如果有补充 ms-、webkit- 前缀
export const autoprefixer = function (style) {
  if (typeof style !== 'object') return style
  const rules = ['transform', 'transition', 'animation']
  const prefixes = ['ms-', 'webkit-']
  rules.forEach((rule) => {
    const value = style[rule]
    if (rule && value) {
      // 为 style 对象补充前缀 ms-、webkit-
      prefixes.forEach((prefix) => {
        style[prefix + rule] = value
      })
    }
  })
  return style
}

// 驼峰风格命名 转 kebab 命名
export const kebabCase = function (str) {
  // 正则表达式，匹配非连字符(-)后紧跟大写字母的位置
  const hyphenateRE = /([^-])([A-Z])/g
  // 将匹配到的位置插入连字符(-)，并转换为小写字母，确保所有大写字母前都添加了连字符
  // $1 表示第一个捕获组的内容，即非连字符(-)的部分
  // $2 表示第二个捕获组的内容，即大写字母部分
  return (
    str
      // 第一次替换没有完全覆盖所有情况：
      // 1. 如果要替换的大写字母个数是偶数
      // 比如 HH 替换为 H-H 是可以的；
      // 2. 如果要替换的大写字母个数是奇数
      // 比如 HHH 会被替换为 H-HH 还需要再执行一次
      .replace(hyphenateRE, '$1-$2')
      // 第二次执行可以保证没有遗漏的情况
      .replace(hyphenateRE, '$1-$2')
      // 再将整个字符串转换为小写字母
      .toLowerCase()
  )
}

// 这里仅仅是将字符串的首字母大写，其他字符不变
export const capitalize = function (str) {
  if (!isString(str)) return str
  // 这里为什么使用 charAt(0) 而不是 str[0] 呢？
  // [] 索引器访问不存在的属性会返回 undefined，而 chatAt(0) 返回空字符串
  // ''.charAt(0) => ''    ''[0] => undefined
  // ''.slice(1) => ''
  return str.charAt(0).toUpperCase() + str.slice(1)
}

// 不严格地判断两个值是否相等
export const looseEqual = function (a, b) {
  // 这里 Date、Math 等对象，会被判定为 isObject: false
  const isObjectA = isObject(a)
  const isObjectB = isObject(b)

  if (isObjectA && isObjectB) {
    // 普通的对象，可以通过 JSON.stringify 转换成字面量再去比较
    return JSON.stringify(a) === JSON.stringify(b)
  } else if (!isObjectA && !isObjectB) {
    // 非对象类型可以通过 String() 转换成字面量再去比较
    return String(a) === String(b)
  } else {
    return false
  }
}

export const arrayEquals = function (arrayA, arrayB) {
  arrayA = arrayA || []
  arrayB = arrayB || []

  if (arrayA.length !== arrayB.length) {
    return false
  }

  for (let i = 0; i < arrayA.length; i++) {
    if (!looseEqual(arrayA[i], arrayB[i])) {
      return false
    }
  }

  return true
}

export const isEqual = function (value1, value2) {
  if (Array.isArray(value1) && Array.isArray(value2)) {
    return arrayEquals(value1, value2)
  }
  return looseEqual(value1, value2)
}

// 判断对象是否为空
export const isEmpty = function (val) {
  // 检查是否为null或undefined
  if (val == null) return true

  // 布尔类型永远不为空
  if (typeof val === 'boolean') return false

  // 数字0被认为是空
  if (typeof val === 'number') return !val

  // Error对象，如果message为空则认为是空
  if (val instanceof Error) return val.message === ''

  switch (Object.prototype.toString.call(val)) {
    // 字符串或数组，如果长度为0则认为是空
    case '[object String]':
    case '[object Array]':
      return !val.length

    // Map, Set或File对象，如果size为0则认为是空
    case '[object File]':
    case '[object Map]':
    case '[object Set]': {
      return !val.size
    }
    // 普通对象，如果没有键则认为是空
    case '[object Object]': {
      return !Object.keys(val).length
    }
  }

  return false
}

/**
 * 使用 requestAnimationFrame 进行函数节流
 * 限制一帧最多执行一次
 *
 * @param {Function} fn - 需要节流的函数
 * @returns {Function} - 返回节流后的函数
 *
 * @description
 * 该函数通过 requestAnimationFrame 实现了一个节流机制，用于限制函数的执行频率。
 * 当返回的函数被调用时，如果之前的调用尚未完成（即 locked 为 true），则不会立即执行函数 fn。
 * 相反，它会等待下一帧的刷新（由 requestAnimationFrame 触发），然后执行 fn 并重置 locked 状态。
 * 这种方法特别适用于与动画或滚动事件相关的场景，可以帮助提高性能并减少不必要的渲染。
 */
export function rafThrottle(fn) {
  let locked = false
  return function (...args) {
    if (locked) return
    locked = true
    window.requestAnimationFrame((_) => {
      fn.apply(this, args)
      locked = false
    })
  }
}

/**
 * 将对象转换为数组。
 *
 * @param {Object|Array} obj - 需要转换的对象或数组。
 * @returns {Array} - 转换后的数组。
 *
 * 如果传入的对象已经是数组，则直接返回该数组。
 * 如果传入的对象为空（通过isEmpty函数判断），则返回一个空数组。
 * 否则，将对象放入一个数组中并返回该数组。
 *
 */
export function objToArray(obj) {
  if (Array.isArray(obj)) {
    return obj
  }
  return isEmpty(obj) ? [] : [obj]
}

/**
 * 深度克隆一个对象或数组
 *
 * @param {any} obj 要克隆的对象或数组
 * @returns {any} 克隆后的对象或数组
 *
 * 简化的深拷贝，不考虑循环引用等复杂情况
 */
export function cloneDeep(obj) {
  // 如果 obj 是 函数 typeof obj 就是 'function'
  // 所以 null、基本数据类型 和 函数 都是直接返回
  if (obj === null || typeof obj !== 'object') {
    return obj
  }

  // 数组和对象的结构不一样，所以分开处理
  // 但是对象还有类似 Date、RegExp 等内置对象
  // 这里都会统一被处理为 {}
  // 这里是不严谨的
  let clonedObj = Array.isArray(obj) ? [] : {}

  // 由于数组和对象都可以通过 for in 和索引器访问或复制
  // 所以可以实现对象的复制
  for (let key in obj) {
    if (obj.hasOwnProperty(key)) {
      // 这里使用递归调用，实现深拷贝
      // 关键问题在这里，如果对象存在循环引用，会导致栈溢出
      clonedObj[key] = cloneDeep(obj[key])
    }
  }

  return clonedObj
}
